use std::sync::{Arc, Mutex};

use axum::{
    extract::{ws::WebSocket, State, WebSocketUpgrade},
    http::status::StatusCode,
    response::{Html, IntoResponse},
    routing::get,
    Router,
};
use cargo_metadata::CompilerMessage;
use futures_util::{SinkExt, StreamExt};
use tokio::sync::broadcast::{Receiver, Sender};

use crate::{dev::controller, Database};

use super::{CreateRustAppMigration, DevServerEvent};

/// TODO: use a state machine or refactor into an enum
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
struct CurrentDevState {
    backend_status: bool,
    backend_compiled: bool,
    backend_compiling: bool,
    backend_restarting: bool,
    compiler_messages: Vec<CompilerMessage>,
    vite_status: bool,
    features: Vec<String>,
    migrations_pending: (bool, Vec<CreateRustAppMigration>),
}

struct AppState {
    #[allow(dead_code)]
    project_dir: &'static str,
    rx: tokio::sync::Mutex<Receiver<DevServerEvent>>, // this is the original subscribed receiver which hasn't missed a single event :)
    tx: Sender<DevServerEvent>,
    file_tx: Sender<String>,
    db: Database,
    dev: Mutex<CurrentDevState>,
}

pub async fn start(
    project_dir: &'static str,
    dev_port: u16,
    dev_server_events_r: Receiver<DevServerEvent>,
    dev_server_events_s: Sender<DevServerEvent>,
    file_events_s: Sender<String>,
    features: Vec<String>,
) {
    assert!(
        dotenv::dotenv().is_ok(),
        "ERROR: Could not load environment variables from dotenv file"
    );

    let app_state = Arc::new(AppState {
        project_dir,
        rx: tokio::sync::Mutex::new(dev_server_events_r),
        tx: dev_server_events_s,
        file_tx: file_events_s,
        db: Database::new(),
        dev: Mutex::new(CurrentDevState {
            backend_compiled: true,
            backend_compiling: false,
            backend_restarting: false,
            backend_status: true,
            compiler_messages: vec![],
            vite_status: true,
            features,
            migrations_pending: (false, vec![]),
        }),
    });

    let app = Router::new()
        .route("/", get(|| async {
            // Let user know the server is running
            Html(r#"
                <html style="background-color: #171717; height: 100%; width: 100%;">
                    <head><title>Create Rust App: Development Server</title></head>
                    <body style="text-align: center; color: white; fony-family: sans-serif; padding: 50px;">
                        <img style="margin-right: 12px" height="50px" src="" alt="" />
                        <h3>Create Rust App: Development Server</h3>
                    </body>
                </html>
            "#)
        }))
        .route("/vitejs-down", get(vitejs_down_handler))
        .route("/vitejs-up", get(vitejs_up_handler))
        .route("/backend-up", get(backend_up_handler))
        .route("/ws", get(ws_handler))
        .with_state(app_state);

    println!("Starting dev server @ http://localhost:{dev_port}/");
    let listener = tokio::net::TcpListener::bind(&format!("0.0.0.0:{dev_port}"))
        .await
        .unwrap();
    axum::serve(listener, app.into_make_service())
        .await
        .unwrap();
}

async fn backend_up_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
    let sender = state.tx.clone();
    sender.send(DevServerEvent::BackendRestarting(false)).ok();
    sender.send(DevServerEvent::BackendStatus(true)).ok();

    StatusCode::OK.into_response()
}

async fn vitejs_up_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
    let sender = state.tx.clone();
    sender.send(DevServerEvent::ViteJSStatus(true)).ok();

    StatusCode::OK.into_response()
}

async fn vitejs_down_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
    let sender = state.tx.clone();
    sender.send(DevServerEvent::ViteJSStatus(true)).ok();

    StatusCode::OK.into_response()
}

async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> impl IntoResponse {
    ws.on_upgrade(|socket| handle_socket(socket, state))
}

/// TODO break this function up into smaller sub functions
#[allow(clippy::too_many_lines)]
async fn handle_socket(stream: WebSocket, state: Arc<AppState>) {
    use axum::extract::ws::Message;
    let (mut sender, mut receiver) = stream.split();

    /*
        SECTION: sending initial state
    */
    let compiler_messages = state.dev.lock().unwrap().compiler_messages.clone();
    let backend_status = state.dev.lock().unwrap().backend_status;
    let backend_compiling = state.dev.lock().unwrap().backend_compiling;
    let backend_restarting = state.dev.lock().unwrap().backend_restarting;
    let backend_compiled = state.dev.lock().unwrap().backend_compiled;
    let vite_status = state.dev.lock().unwrap().vite_status;
    let features = state.dev.lock().unwrap().features.clone();
    let migrations_pending = state.dev.lock().unwrap().migrations_pending.clone();
    sender
        .send(Message::Text(DevServerEvent::FeaturesList(features).json()))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::CompileSuccess(backend_compiled).json(),
        ))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::CompileMessages(compiler_messages).json(),
        ))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::BackendStatus(backend_status).json(),
        ))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::BackendRestarting(backend_restarting).json(),
        ))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::BackendCompiling(backend_compiling).json(),
        ))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::ViteJSStatus(vite_status).json(),
        ))
        .await
        .unwrap();
    sender
        .send(Message::Text(
            DevServerEvent::PendingMigrations(migrations_pending.0, migrations_pending.1).json(),
        ))
        .await
        .unwrap();

    /*
        SECTION: receive dev server events
    */
    let db = state.db.clone();
    let state2 = state.clone();
    let dse_s = state2.tx.clone();
    let mut send_task = tokio::spawn(async move {
        let mut rx = state2.rx.lock().await;

        while let Ok(e) = rx.recv().await {
            let mut send_response = true;

            match e.clone() {
                DevServerEvent::CHECK_MIGRATIONS => {
                    send_response = false;
                    let migrations = controller::get_migrations(&db);
                    if controller::needs_migration(&db) {
                        dse_s
                            .send(DevServerEvent::PendingMigrations(true, migrations))
                            .ok();
                    } else {
                        dse_s
                            .send(DevServerEvent::PendingMigrations(false, migrations))
                            .ok();
                    }
                }
                DevServerEvent::MigrationResponse(success, _) => {
                    // this is a response; we don't need to handle anything on the dev-server side
                    let migrations = controller::get_migrations(&db);
                    dse_s
                        .send(DevServerEvent::PendingMigrations(!success, migrations))
                        .ok();
                }
                DevServerEvent::PendingMigrations(a, b) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.migrations_pending = (a, b);
                }
                DevServerEvent::FeaturesList(list) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.features = list;
                }
                DevServerEvent::BackendCompiling(b) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.backend_compiling = b;
                }
                DevServerEvent::BackendStatus(b) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.backend_status = b;
                }
                DevServerEvent::BackendRestarting(b) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.backend_restarting = b;
                }
                DevServerEvent::CompileSuccess(b) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.backend_compiled = b;
                }
                DevServerEvent::CompileMessages(messages) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.compiler_messages.clone_from(&messages);
                }
                DevServerEvent::SHUTDOWN => {
                    let mut s = state2.dev.lock().unwrap();
                    s.backend_status = false;
                }
                DevServerEvent::ViteJSStatus(b) => {
                    let mut s = state2.dev.lock().unwrap();
                    s.vite_status = b;
                }
            };

            if send_response {
                sender.send(Message::Text(e.json())).await.unwrap();
            }
        }
    });

    /*
        SECTION: receive websocket events
    */
    let state3 = state.clone();
    let file_tx = state.file_tx.clone();
    let mut recv_task = tokio::spawn(async move {
        let state3 = state3.clone();

        while let Some(msg) = receiver.next().await {
            if let Ok(msg) = msg {
                match msg {
                    Message::Text(t) => {
                        let state3 = state3.clone();
                        let file_tx = file_tx.clone();
                        tokio::spawn(async move {
                            let state3 = state3.clone();
                            if t.starts_with("open:") {
                                // HACK: tell the backend server we're about the modify the file (this is a side effect of the `open` crate)
                                //       that way it won't try to recompile as a result of this filesystem modification event

                                let (_, file_name) = t.split_at(5);
                                file_tx.send(file_name.to_string()).ok();

                                // WARNING: this hack causes a race condition between the file above being registered for ignoring
                                //          and the `open` command below which will modify the file
                                //
                                // suggestion 1: change the method by which we open files so they don't get "modified" when opening them
                                //               the new method should be one which can open a specific line and column, unlike the current solution
                                //
                                // suggestion 2: listen for a 'file-registered' event from the backend compiling server
                                //               so we know that it won't re-compile based on the modify event that this
                                //               file opening causes
                                open::that(file_name).unwrap_or_else(|_| {
                                    println!("📝 Could not open file `{file_name}`");
                                });
                            } else if t.eq_ignore_ascii_case("migrate") {
                                let (success, error_message) =
                                    match controller::migrate_db(&state3.db) {
                                        Ok(_) => (true, None),
                                        Err(e) => (false, Some(e.to_string())),
                                    };

                                state3
                                    .tx
                                    .send(DevServerEvent::MigrationResponse(success, error_message))
                                    .ok();
                            }
                        });
                    }
                    Message::Binary(_)
                    | Message::Ping(_)
                    | Message::Pong(_)
                    | Message::Close(_) => {}
                }
            }
        }
    });

    // If any one of the tasks exit, abort the other.
    tokio::select! {
        _ = (&mut send_task) => {recv_task.abort()},
        _ = (&mut recv_task) => {send_task.abort()},
    };
}
